﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.CodeDom;
using System.Collections;

namespace System.ComponentModel.Design.Serialization;

public sealed partial class CodeDomComponentSerializationService
{
    private sealed partial class CodeDomSerializationStore
    {
        /// <summary>
        ///  This is a simple code dom serializer that serializes a set of objects as a unit.
        /// </summary>
        private class ComponentListCodeDomSerializer : CodeDomSerializer
        {
            internal static readonly ComponentListCodeDomSerializer s_instance = new();
            private readonly Dictionary<string, OrderedCodeStatementCollection?> _statementsTable = [];
            private readonly Dictionary<string, List<CodeExpression>> _expressions = [];
            private Dictionary<string, CodeDomComponentSerializationState>? _objectState; // only used during deserialization
            private bool _applyDefaults = true;
            private readonly HashSet<string> _nameResolveGuard = [];

            public override object Deserialize(IDesignerSerializationManager manager, object state)
            {
                throw new NotSupportedException();
            }

            private static void PopulateCompleteStatements(object? data, string name, CodeStatementCollection completeStatements, Dictionary<string, List<CodeExpression>> expressions)
            {
                if (data is null)
                {
                    return;
                }

                if (data is CodeStatementCollection statements)
                {
                    completeStatements.AddRange(statements);
                }
                else if (data is CodeStatement statement)
                {
                    completeStatements.Add(statement);
                }
                else if (data is CodeExpression expression)
                {
                    // we handle expressions a little differently since they don't have a LHS or RHS
                    // they won't show up correctly in the statement table. We will deserialize them explicitly.
                    if (!expressions.TryGetValue(name, out List<CodeExpression>? exps))
                    {
                        exps = [];
                        expressions[name] = exps;
                    }

                    exps.Add(expression);
                }
                else
                {
                    Debug.Fail($"No case for {data.GetType().Name}");
                }
            }

            /// <summary>
            ///  Deserializes the given object state. The results are contained within the serialization manager's
            ///  name table. The objectNames list is used to deserialize in the proper order,
            ///  as objectState is unordered.
            /// </summary>
            internal void Deserialize(IDesignerSerializationManager manager, Dictionary<string, CodeDomComponentSerializationState> objectState, List<string> objectNames, bool applyDefaults)
            {
                CodeStatementCollection completeStatements = [];
                _expressions.Clear();
                _applyDefaults = applyDefaults;
                foreach (string name in objectNames)
                {
                    if (objectState.TryGetValue(name, out CodeDomComponentSerializationState? state))
                    {
                        PopulateCompleteStatements(state.Code, name, completeStatements, _expressions);
                        PopulateCompleteStatements(state.Context, name, completeStatements, _expressions);
                    }
                }

                CodeStatementCollection mappedStatements = [];
                CodeMethodMap methodMap = new(mappedStatements);

                methodMap.Add(completeStatements);
                methodMap.Combine();
                _statementsTable.Clear();

                // generate statement table keyed on component name
                FillStatementTable(manager, _statementsTable, mappedStatements);

                // We need to also ensure that for every entry in the statement table we have a
                // corresponding entry in objectNames. Otherwise, we won't deserialize completely.
                HashSet<string> completeNames = [..objectNames];
                completeNames.UnionWith(_statementsTable.Keys);

                _objectState = new(objectState);

                ResolveNameEventHandler resolveNameHandler = new(OnResolveName);
                manager.ResolveName += resolveNameHandler;
                try
                {
                    foreach (string name in completeNames)
                    {
                        ResolveName(manager, name, true);
                    }
                }
                finally
                {
                    _objectState = null;
                    manager.ResolveName -= resolveNameHandler;
                }
            }

            private void OnResolveName(object? sender, ResolveNameEventArgs e)
            {
                string name = e.Name!;
                // note: this recursion guard does not fix the problem,
                // but rather avoids a stack overflow which will bring down VS and cause loss of data.
                if (!_nameResolveGuard.Add(name))
                {
                    return;
                }

                try
                {
                    IDesignerSerializationManager manager = (IDesignerSerializationManager)sender!;
                    if (ResolveName(manager, name, false))
                    {
                        e.Value = manager.GetInstance(name);
                    }
                }
                finally
                {
                    _nameResolveGuard.Remove(name);
                }
            }

            private void DeserializeDefaultProperties(IDesignerSerializationManager manager, string name, List<string>? defProps)
            {
                // Next, default properties, but only if we successfully resolved.
                if (defProps is null || !_applyDefaults)
                {
                    return;
                }

                object? comp = manager.GetInstance(name);
                if (comp is null)
                {
                    return;
                }

                PropertyDescriptorCollection props = TypeDescriptor.GetProperties(comp);
                foreach (string propName in defProps)
                {
                    PropertyDescriptor? prop = props[propName];
                    if (prop is not null && prop.CanResetValue(comp))
                    {
                        // If there is a member relationship setup for this property, we should disconnect it first.
                        // This makes sense, since if there was a previous relationship,
                        // we would have serialized it and not come here at all.
                        if (manager.TryGetService(out MemberRelationshipService? relationships) && relationships[comp, prop] != MemberRelationship.Empty)
                        {
                            relationships[comp, prop] = MemberRelationship.Empty;
                        }

                        prop.ResetValue(comp);
                    }
                }
            }

            private static void DeserializeDesignTimeProperties(IDesignerSerializationManager manager, string name, Dictionary<string, object?>? state)
            {
                if (state is null)
                {
                    return;
                }

                object? comp = manager.GetInstance(name);
                if (comp is null)
                {
                    return;
                }

                PropertyDescriptorCollection props = TypeDescriptor.GetProperties(comp);

                foreach (KeyValuePair<string, object?> stateEntry in state)
                {
                    PropertyDescriptor? prop = props[stateEntry.Key];
                    prop?.SetValue(comp, stateEntry.Value);
                }
            }

            /// <summary>
            ///  This is used to resolve nested component references.
            ///  NestedComponents don't exist as sited components within the DesignerHost,
            ///  they are actually sited within a parent component.
            ///  This method takes the FullName defined on INestedSite and returns the component which matches it.
            ///  outerComponent is the name of the topmost component which does exist in the DesignerHost
            ///  This code also exists in VSCodeDomDesignerLoader -- please keep them in sync.
            /// </summary>
            private static IComponent? ResolveNestedName(IDesignerSerializationManager? manager, string name, [NotNullIfNotNull(nameof(manager))] out string? outerComponent)
            {
                if (manager is null)
                {
                    outerComponent = null;
                    return null;
                }

                bool moreChunks;
                // We need to resolve the first chunk using the manager. other chunks will be resolved within the nested containers.
                int curIndex = name.IndexOf('.');
                Debug.Assert(curIndex > 0, "ResolvedNestedName accepts only nested names!");
                outerComponent = name[..curIndex];
                IComponent? curComp = manager.GetInstance(outerComponent) as IComponent;

                do
                {
                    int prevIndex = curIndex;
                    curIndex = name.IndexOf('.', curIndex + 1);

                    moreChunks = curIndex != -1;
                    string compName = moreChunks
                        ? name.Substring(prevIndex + 1, curIndex)
                        : name[(prevIndex + 1)..];

                    if (string.IsNullOrEmpty(compName))
                    {
                        return null;
                    }

                    ISite? site = curComp?.Site;
                    if (!site.TryGetService(out INestedContainer? container))
                    {
                        return null;
                    }

                    curComp = container.Components[compName];
                }
                while (moreChunks);

                return curComp;
            }

            private bool ResolveName(IDesignerSerializationManager manager, string name, bool canInvokeManager)
            {
                bool resolved = false;
                // Check for a nested name. Components that are sited within NestedContainers need to be looked up
                // in their nested container, and won't be resolvable directly via the manager.
                if (name.IndexOf('.') > 0)
                {
                    IComponent? nestedComp = ResolveNestedName(manager, name, out string? parentName);
                    if (nestedComp is not null && parentName is not null)
                    {
                        manager.SetName(nestedComp, name);
                        // What is the point of this?
                        // Well, the nested components won't be in the statement table with its nested name.
                        // However, their most parent component will be, so forcing a resolve of their name
                        // will actually deserialize the nested statements.
                        ResolveName(manager, parentName, canInvokeManager);
                    }
                    else
                    {
                        Debug.Fail($"Unable to resolve nested component: {name}");
                    }
                }

                // First we check to see if the statements table contains an OrderedCodeStatementCollection for this name.
                // If it does this means we have not resolved this name yet, so we grab its
                // OrderedCodeStatementCollection and deserialize that, along with any default properties
                // and design-time properties.
                // If it doesn't contain an OrderedCodeStatementsCollection this means one of two things:
                // 1. We already resolved this name and shoved an instance in there.
                //    In this case we just return the instance
                // 2. There are no statements corresponding to this name,
                //    but there might be expressions that have never been deserialized,
                //    so we check for that and deserialize those.
                _statementsTable.TryGetValue(name, out OrderedCodeStatementCollection? statements);
                if (statements is not null)
                {
                    _statementsTable[name] = null; // prevent recursion
                    // we look through the statements to find the variableRef or fieldRef that matches this name
                    string? typeName = null;
                    foreach (CodeStatement statement in statements)
                    {
                        if (statement is CodeVariableDeclarationStatement codeVariableDeclaration)
                        {
                            typeName = codeVariableDeclaration.Type.BaseType;
                            break;
                        }
                    }

                    // next, invoke the serializer for this component
                    if (typeName is not null)
                    {
                        Type? type = manager.GetType(typeName);
                        if (type is null)
                        {
                            manager.ReportError(new CodeDomSerializerException(string.Format(SR.SerializerTypeNotFound, typeName), manager));
                        }
                        else if (statements.Count > 0)
                        {
                            CodeDomSerializer? serializer = GetSerializer(manager, type);
                            if (serializer is null)
                            {
                                // We report this as an error. This indicates that there are code statements
                                // in initialize component that we do not know how to load.
                                manager.ReportError(new CodeDomSerializerException(
                                    string.Format(SR.SerializerNoSerializerForComponent, type.FullName), manager));
                            }
                            else
                            {
                                try
                                {
                                    object? instance = serializer.Deserialize(manager, statements);
                                    resolved = instance is not null;
                                    if (resolved)
                                    {
                                        _statementsTable[name] = (OrderedCodeStatementCollection?)instance;
                                    }
                                }
                                catch (Exception ex)
                                {
                                    manager.ReportError(ex);
                                }
                            }
                        }
                    }

                    // if we can't find a typeName to get a serializer with we fallback to deserializing
                    // each statement individually using the default serializer.
                    else
                    {
                        foreach (CodeStatement cs in statements)
                        {
                            DeserializeStatement(manager, cs);
                        }

                        resolved = true;
                    }

                    if (_objectState!.Remove(name, out CodeDomComponentSerializationState? state))
                    {
                        DeserializeState(manager, name, state);
                    }

                    if (_expressions.Remove(name, out List<CodeExpression>? exps))
                    {
                        foreach (CodeExpression exp in exps)
                        {
                            DeserializeExpression(manager, name, exp);
                        }

                        resolved = true;
                    }
                }
                else
                {
                    resolved = ((IDictionary)_statementsTable)[name] is not null;
                    if (!resolved)
                    {
                        // this is condition 2 of the comment at the start of this method.
                        if (_expressions.TryGetValue(name, out List<CodeExpression>? exps))
                        {
                            foreach (CodeExpression exp in exps)
                            {
                                object? exValue = DeserializeExpression(manager, name, exp);
                                if (exValue is not null && !resolved && canInvokeManager && manager.GetInstance(name) is null)
                                {
                                    manager.SetName(exValue, name);
                                    resolved = true;
                                }
                            }
                        }

                        // Sometimes components won't be in either the statements table or the expressions table,
                        // for example, this occurs for resources during undo/redo.
                        // In these cases the component should be resolvable by the manager.
                        // Never do this when we have been asked by the serialization manager to resolve the name;
                        // otherwise we may infinitely recurse.
                        if (!resolved && canInvokeManager)
                        {
                            resolved = manager.GetInstance(name) is not null;
                        }

                        // In this case we still need to correctly deserialize default properties &  design-time only properties.
                        if (resolved && _objectState!.TryGetValue(name, out CodeDomComponentSerializationState? state))
                        {
                            DeserializeState(manager, name, state);
                        }
                    }

                    if (!resolved && canInvokeManager)
                    {
                        manager.ReportError(new CodeDomSerializerException(string.Format(SR.CodeDomComponentSerializationServiceDeserializationError, name), manager));
                        Debug.Fail($"No statements or instance for name and no lone expressions: {name}");
                    }
                }

                return resolved;
            }

            private void DeserializeState(IDesignerSerializationManager manager, string name, CodeDomComponentSerializationState state)
            {
                DeserializeDefaultProperties(manager, name, state.Properties);
                DeserializeDesignTimeProperties(manager, name, state.Resources);
                DeserializeEventResets(manager, name, state.Events);
                DeserializeModifier(manager, name, state.Modifier);
            }

            private static void DeserializeEventResets(IDesignerSerializationManager? manager, string name, List<string>? eventNames)
            {
                if (eventNames is not null && manager is not null && !string.IsNullOrEmpty(name))
                {
                    object? comp = manager.GetInstance(name);
                    if (comp is not null && manager.GetService(typeof(IEventBindingService)) is IEventBindingService ebs)
                    {
                        PropertyDescriptorCollection eventProps = ebs.GetEventProperties(TypeDescriptor.GetEvents(comp));
                        if (eventProps is not null)
                        {
                            foreach (string eventName in eventNames)
                            {
                                PropertyDescriptor? prop = eventProps[eventName];

                                prop?.SetValue(comp, null);
                            }
                        }
                    }
                }
            }

            private static void DeserializeModifier(IDesignerSerializationManager manager, string name, object? state)
            {
                if (state is null)
                {
                    return;
                }

                Debug.Assert(state is MemberAttributes, "Attempting to deserialize a null modifier");
                object? comp = manager.GetInstance(name);
                if (comp is not null)
                {
                    MemberAttributes modifierValue = (MemberAttributes)state;
                    PropertyDescriptor? modifierProp = TypeDescriptor.GetProperties(comp)["Modifiers"];
                    modifierProp?.SetValue(comp, modifierValue);
                }
            }

            public override object Serialize(IDesignerSerializationManager manager, object state)
            {
                throw new NotSupportedException();
            }

            /// <summary>
            ///  For everything in the serialization manager's container, we need a variable ref,
            ///  just in case something that has changed has a reference to another object. We also
            ///  must do this for everything that we are serializing that is not marked as EntireObject.
            ///  Otherwise reference could leak and cause the entire object to be serialized.
            /// </summary>
            internal void SetupVariableReferences(IDesignerSerializationManager manager, IContainer container, Dictionary<object, ObjectData> objectData, IList shimObjectNames)
            {
                foreach (IComponent c in container.Components)
                {
                    string? name = TypeDescriptor.GetComponentName(c);
                    if (!string.IsNullOrEmpty(name))
                    {
                        bool needVar = !(objectData.TryGetValue(c, out ObjectData? data) && data.EntireObject);

                        if (needVar)
                        {
                            CodeVariableReferenceExpression var = new(name);
                            SetExpression(manager, c, var);
                            if (!shimObjectNames.Contains(name))
                            {
                                shimObjectNames.Add(name);
                            }

                            if (c.Site.TryGetService(out INestedContainer? nested) && nested.Components.Count > 0)
                            {
                                SetupVariableReferences(manager, nested, objectData, shimObjectNames);
                            }
                        }
                    }
                }
            }

            /// <summary>
            ///  Serializes the given set of objects (contained in objectData) into the given object state dictionary.
            /// </summary>
            internal void Serialize(IDesignerSerializationManager manager, Dictionary<object, ObjectData> objectData, Dictionary<string, CodeDomComponentSerializationState> objectState, IList shimObjectNames)
            {
                if (manager.GetService<IContainer>() is { } container)
                {
                    SetupVariableReferences(manager, container, objectData, shimObjectNames);
                }

                // Next, save a statement collection for each object.
                StatementContext statementCtx = new();
                statementCtx.StatementCollection.Populate(objectData.Keys);
                manager.Context.Push(statementCtx);
                try
                {
                    foreach (ObjectData data in objectData.Values)
                    {
                        object? code = null;
                        CodeStatementCollection? ctxStatements = null;
                        Dictionary<string, object?>? resources = null;
                        CodeStatementCollection extraStatements = [];
                        manager.Context.Push(extraStatements);
                        if (manager.TryGetSerializer(data._value.GetType(), out CodeDomSerializer? serializer))
                        {
                            if (data.EntireObject)
                            {
                                if (!IsSerialized(manager, data._value))
                                {
                                    code = data.Absolute
                                        ? serializer.SerializeAbsolute(manager, data._value)
                                        : serializer.Serialize(manager, data._value);

                                    ctxStatements = statementCtx.StatementCollection[data._value];
                                    if (ctxStatements?.Count == 0)
                                    {
                                        ctxStatements = null;
                                    }

                                    if (extraStatements.Count > 0 && code is CodeStatementCollection existingStatements)
                                    {
                                        existingStatements.AddRange(extraStatements);
                                    }
                                }
                                else
                                {
                                    code = statementCtx.StatementCollection[data._value];
                                }
                            }
                            else
                            {
                                CodeStatementCollection codeStatements = [];
                                foreach (MemberData md in data.Members)
                                {
                                    if (md._member.Attributes.Contains(DesignOnlyAttribute.Yes))
                                    {
                                        // For design time properties, we write their value into a resource blob.
#pragma warning disable SYSLIB0050 // Type or member is obsolete
                                        if (md._member is PropertyDescriptor prop && prop.PropertyType.IsSerializable)
                                        {
                                            resources ??= [];

                                            resources[prop.Name] = prop.GetValue(data._value);
                                        }
#pragma warning restore SYSLIB0050
                                    }
                                    else
                                    {
                                        codeStatements.AddRange(md._absolute
                                            ? serializer.SerializeMemberAbsolute(manager, data._value, md._member)
                                            : serializer.SerializeMember(manager, data._value, md._member));
                                    }
                                }

                                code = codeStatements;
                            }
                        }

                        if (extraStatements.Count > 0)
                        {
                            if (code is CodeStatementCollection existingStatements)
                            {
                                existingStatements.AddRange(extraStatements);
                            }
                        }

                        manager.Context.Pop();
                        // And now search for default properties and events
                        List<string>? defaultPropList = null;
                        List<string>? defaultEventList = null;
                        IEventBindingService? ebs = manager.GetService<IEventBindingService>();
                        if (data.EntireObject)
                        {
                            PropertyDescriptorCollection props = TypeDescriptor.GetProperties(data._value);
                            foreach (PropertyDescriptor prop in props)
                            {
                                if (!prop.ShouldSerializeValue(data._value)
                                    && !prop.Attributes.Contains(DesignerSerializationVisibilityAttribute.Hidden))
                                {
                                    if (!prop.IsReadOnly || prop.Attributes.Contains(DesignerSerializationVisibilityAttribute.Content))
                                    {
                                        defaultPropList ??= new(data.Members.Count);
                                        defaultPropList.Add(prop.Name);
                                    }
                                }
                            }

                            if (ebs is not null)
                            {
                                PropertyDescriptorCollection events = ebs.GetEventProperties(TypeDescriptor.GetEvents(data._value));
                                foreach (PropertyDescriptor eventProp in events)
                                {
                                    if (eventProp is null || eventProp.IsReadOnly)
                                    {
                                        continue;
                                    }

                                    if (eventProp.GetValue(data._value) is null)
                                    {
                                        defaultEventList ??= [];

                                        defaultEventList.Add(eventProp.Name);
                                    }
                                }
                            }
                        }
                        else
                        {
                            foreach (MemberData md in data.Members)
                            {
                                if (md._member is PropertyDescriptor prop && !prop.ShouldSerializeValue(data._value))
                                {
                                    if (ebs?.GetEvent(prop) is not null)
                                    {
                                        Debug.Assert(prop.GetValue(data._value) is null, "ShouldSerializeValue and GetValue are differing");
                                        defaultEventList ??= [];

                                        defaultEventList.Add(prop.Name);
                                    }
                                    else
                                    {
                                        defaultPropList ??= new(data.Members.Count);
                                        defaultPropList.Add(prop.Name);
                                    }
                                }
                            }
                        }

                        // Check for non-default modifiers property
                        object? modifier = TypeDescriptor.GetProperties(data._value)["Modifiers"]?.GetValue(data._value);

                        if (code is not null || defaultPropList is not null)
                        {
                            objectState[data._name] = new CodeDomComponentSerializationState(code, ctxStatements, defaultPropList, resources, defaultEventList, modifier);
                        }
                    }
                }
                finally
                {
                    manager.Context.Pop();
                }
            }
        }
    }
}
